Skip to content

fix(console): show the producer’s localized userMessage on metadata-save advisories and refusals - #33576

Queued
objectstack-fleet[bot] wants to merge 2 commits into
mainfrom
claude/project-thread-bhbxol
Queued

objectstack-fleet[bot] wants to merge 2 commits into
mainfrom
claude/project-thread-bhbxol

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Leehom · project thread

Fixes #32657

Refs #33524 (console half pinned, no console change; the card stays open for its server half)

Clause-②: yes (widening)
Metadata-protocol: none

维护者速读

What changes

Layer 3 of tracking #32654 (ruling C on #32440, 「32440 选C,32043 一起按C处理」): the console renders the user-facing text the producer composed in the request's language, with no per-code key table of its own.

  • Advisory toast (saveAdvisoryToast.ts, formatFinding, @object-ui/app-shell). An advisory finding that carries userMessage (RuntimeAuthoringIssueSchema.userMessage, which the save and publish doors compose since feat(metadata-protocol): a metadata write's advisories and refusals come back in the caller's language (#32656, 1/2) #33276) renders as [rule] where — USER_MESSAGE, in place of the developer-facing English message and hint. The rule id and where are locators and frame both faces. A finding without one keeps [rule] where — message hint. The module header now says which prose is shown.
  • The one issue reader (metadata-error.ts, formatMetadataIssue, @object-ui/data-objectstack). A 422 issue with userMessage renders • path — USER_MESSAGE; one without keeps • path — message. formatMetadataError and app-shell's formatPublishFailures both write issues through it, so the banners, the field designer's refusal and the publish-failure list follow.
  • Studio's refusal sentence (metadataError.ts, issueRefusal). When the issue carries userMessage, the sentence's problem part is that text verbatim; only an issue without one has its English message read through translateValidationMessage, the console's own table. This is the console-side translation of server prose the ruling retires where the producer speaks.
  • Type (metadata-client.ts). MetadataValidationIssue gains optional userMessage, typed RuntimeAuthoringIssue['userMessage'] from @objectstack/spec/api, so a rename in the spec stops it compiling. Published interface, optional member: Clause-②: yes (widening).
  • Changeset .changeset/32657-console-localized-user-text.md: @object-ui/app-shell, @object-ui/console, @object-ui/data-objectstack patch, with both declaration lines.

The presence test is the same predicate in all three readers (a non-empty string after trimming, the rule @objectstack/client applies when it lifts userMessage). declaredUserMessage (@object-ui/react) is the one reader of an API ERROR's marking; it was not reused here because a finding element is not an error, @object-ui/data-objectstack sits below @object-ui/react (no import that way), and saveAdvisoryToast.ts imports nothing that renders by design.

Why

Tests

  • New AdapterProvider.advisoryUserMessage-32657.test.tsx (rendered end to end): the real AdapterProvider builds the real adapter, the real SDK client parses a save response from a fetch double, and the real sonner Toaster draws the toast; the assertions read the toast's text on screen.
    • a zh-CN userMessage (the security-private-no-readscope catalog sentence for these params) shows, with the rule id, and neither the English message nor hint shows;
    • an en userMessage shows the same way;
    • CONTROL: a finding without one keeps message + hint + rule.
  • saveAdvisoryToast.test.ts: two cases added (the exact line with a userMessage; CONTROL: a blank one is no sentence).
  • New metadataError.issueUserMessage-32657.test.ts: formatMetadataIssue with and without userMessage; formatMetadataError and formatPublishFailures carry it; flowSaveRefusal (through issueRefusal) carries a zh userMessage verbatim and not the console's translation, and CONTROL: an issue without one keeps the console's reading.
  • New ModalForm.markedRefusal-33524.test.tsx (rendered; the dialog the console's New and Edit buttons open): the user types and presses the dialog's own Create or Update button, the data source refuses with a 403 shaped as the ObjectStack adapter throws it.
    • create and edit, each with an en and a zh-CN userMessage: the sentence shows, the generic line does not, the developer message does not;
    • CONTROL, create and edit: a 403 with no userMessage shows the generic line and withholds the developer message.
Red-first on base, and ablations

Acceptance notes

Verification

Run on head e0302529 (base origin/main e3f0cfd4).

Build, tests, type-check and lint
  • Builds under the verify lock: pnpm --workspace-concurrency=2 --filter '@objectstack/spec...' --filter '@objectstack/formula...' --filter '@objectstack/client...' --filter '@objectstack/lint...' --filter '@objectstack/sdui-parser...' build (VERDICT command-exit 0), then pnpm --workspace-concurrency=2 --filter '@object-ui/app-shell^...' build (VERDICT command-exit 0).
  • The four new or edited test files: Test Files 4 passed (4), Tests 32 passed (32).
  • Neighbouring suites, pnpm --dir ui exec vitest run --maxWorkers=2 packages/data-objectstack/ packages/app-shell/src/providers/ packages/app-shell/src/views/studio-design/ packages/plugin-designer/src/MetadataFieldsPage.refusalIssues-11302.test.tsx packages/plugin-form/src/ModalForm packages/components/src/__tests__/form-write-error-message.test.tsx: Test Files 499 passed (499), Tests 4343 passed (4343).
  • Type-check, each VERDICT command-exit 0: pnpm --filter @object-ui/data-objectstack run type-check (tsc --noEmit), pnpm --filter @object-ui/app-shell run type-check and pnpm --filter @object-ui/plugin-form run type-check (both tsc --noEmit && tsc -p tsconfig.test.json, so the new test files are compiled).
  • eslint on the eight changed files: 0 errors (8 warnings, all pre-existing no-explicit-any lines in metadata-client.ts). pnpm --dir ui run check:new-line-citations: 0 new citations.
Derived gates (37)

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 37 families on e0302529. All 37 exited 0. pnpm --dir ui exec vitest run scripts/ was SIGKILLed in the loop while a 23-minute suite run held the box; rerun alone under the lock with --maxWorkers=2: Test Files 171 passed, 2 skipped (173), exit 0. --ran reconciliation: 37 derived, 37 run, 0 NOT-MEASURED. Changed lines: 446, under the 3,000 threshold. No dogfood battery: no ! in the title and no (narrowing) arm.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic.
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes.
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: its generated artifacts are regenerated and committed.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English.
  • The Before Submitting list in CONTRIBUTING.md is done.

Development by the card thread session_01FEkQ5m1xDT63aCaQJNjwXD for the seat domain:ui#2.


Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_01FEkQ5m1xDT63aCaQJNjwXD

…-save advisories and refusals

The post-save advisory toast, the metadata issue line (formatMetadataIssue,
behind formatMetadataError and the publish-failure list) and Studio's refusal
sentence now show an advisory's or a 422 issue's userMessage, the sentence the
server composes in the caller's language, in place of the developer-facing
English message. Findings and issues without one keep today's face.
MetadataValidationIssue gains the optional userMessage, typed from the spec's
RuntimeAuthoringIssue.

Adds rendered pins for the advisory toast and for the record create/edit
dialog's marked-refusal reading.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEkQ5m1xDT63aCaQJNjwXD
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 15, 2026
@objectstack-fleet objectstack-fleet Bot added the needs:contract-review Marker only — PR awaits its at-tier Contract review PASS; no check reads it, the record decides label Nov 15, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#2 · os-project-manager · session_01FEkQ5oF8GgTHN2jBZoDwz8

Contract review

Served-tier: TIER_DEFAULT
Head-sha: e030252941226b20d688116cb55785eb34621455
Local-runs: none

Inputs: card #32657 (body, all 4 comments, including claim amendment 6886423383 that sets Clause-②: yes (widening) on MetadataValidationIssue.userMessage), card #33524 (body, both comments, including the premise correction), the PR #33576 body, its 9-file list and net diff against main, and the head's check-runs. Files read at the head and at main with git show only.

① Derived judgments

The diff makes five accept-set or public-surface changes. Each is judged below.

  1. MetadataValidationIssue gains optional userMessage?: RuntimeAuthoringIssue['userMessage'] (ui/packages/data-objectstack/src/metadata-client.ts, barrel-exported from src/index.ts). Right: a pure widening. On main, RuntimeAuthoringIssueSchema.userMessage is z.string().optional() (packages/spec/src/api/protocol.zod.ts), and RuntimeAuthoringIssue is its z.input. The indexed type therefore resolves to string | undefined, and the member is optional. Objects built without it still type-check, and existing readers see one more optional key. The runtime parser stores issues unmapped (metadata-client.ts, the err.issues = issues lift), so the field the server sends reaches MetadataError.issues with no further change. The only type that newly fails to compile is a caller value carrying a non-string userMessage. That is theoretical, and the spec forbids it. Nothing is removed or renamed.
  2. Type import from @objectstack/spec/api. Right: a legitimate edge, and not a new one. @objectstack/spec is already a dependencies entry (workspace:^) of @object-ui/data-objectstack. The import type { …, RuntimeAuthoringIssue } from '@objectstack/spec/api' line is byte-identical on main (it already types advisories/readSaveAdvisories). The diff adds only a use of it. The import is type-only and erased, and ./api is a declared subpath export of the spec. No package.json change is needed, and the diff makes none.
  3. Rendering prefers userMessage over message (and over hint in the toast) in formatFinding (app-shell saveAdvisoryToast.ts), formatMetadataIssue (data-objectstack metadata-error.ts, and through it formatMetadataError and formatPublishFailures) and issueRefusal (app-shell studio-design/metadataError.ts). Right; no relied-on content is lost. This is the behaviour the spec contract prescribes: "absent, a consumer renders message as before. Consumers render it as received; rule stays the stable id and message / hint stay the developer-facing text". It is also ruling C on Studio: an unconfigured nav item vanishes from the navigation editor after reload while still counted as a pending draft; publish dialog shows raw type names in zh #32440. Details:
    • The locators stay on both faces: [rule] where in the toast, and path in the issue line.
    • Machine readers are unaffected, because issues[] and advisories[] are not mutated.
    • CLI, MCP and os validate are untouched.
    • What a person no longer sees is the English developer message (and the toast's hint), for findings that carry a userMessage. That includes the Studio Details disclosure, which is also built by formatMetadataError. This is the ruling's intent: the producer's sentence is the user channel. Should-know, not a defect: an admin who wants the English prose now has to read the network response.
    • The issueRefusal change retires the console-side translateValidationMessage reading only where the producer spoke. Issues without a sentence (every spec-validation issue) keep the old reading.
  4. Empty/whitespace guard (typeof x === 'string' && x.trim() !== '', the same in all three readers). Right.
    • It is the same predicate @objectstack/client applies when it lifts an API error's userMessage (asUserMessage, packages/client/src/index.ts), and the one @object-ui/react's error-message.ts reader uses.
    • A blank sentence falls back to message (plus hint). This is pinned by the saveAdvisoryToast.test.ts CONTROL.
    • A non-blank sentence is rendered untrimmed, which matches the spec's "render it as received".
    • The typeof check also guards a non-string value that reaches a cast issues array at runtime.
  5. ModalForm.markedRefusal-33524.test.tsx (plugin-form). Right: tests only, no product change. It pins the existing declaredUserMessage-before-generic-line reading on create and edit, in en and zh-CN, plus CONTROLs. The PR's red-first record states that it passes on base, which matches the corrected premise of console: the record create/edit dialog shows only "You don't have permission to save this record." on a controlled_by_parent master-edit refusal, dropping the API's reason ("requires edit access to its master record"); #7366 regression on this path #33524.

No must-fix and no should-fix finding in the diff.

② Semver level

The changeset .changeset/32657-console-localized-user-text.md bumps @object-ui/app-shell, @object-ui/console and @object-ui/data-objectstack at patch, with Clause-②: yes (widening) and Metadata-protocol: none as line-initial declarations in both the changeset and the PR body. Matches.

  • Packages. The diff changes production code in app-shell (2 files) and data-objectstack (2 files). @object-ui/console is listed correctly: it is a published package that ships a pre-built SPA bundling @object-ui/app-shell (workspace:*), so its shipped bytes change. @object-ui/plugin-form gains only a test file and publishes nothing new, so it is correctly absent.
  • Level. Under AGENTS.md Post-Task Checklist step 3 the level follows the Metadata-protocol kind alone: none takes patch, and "a non-protocol change, widening or breaking, ships patch". The earlier "Clause-② yes ⇒ ≥ minor" rule (gate census, 2026-09) is superseded by that text on main.
  • Metadata-protocol: none. Correct. Nothing a metadata author writes changes; only which server field the console renders changes.
  • Clause-②: yes (widening). Correct. A new optional member on an exported interface widens the public surface and narrows nothing. It agrees with the claim amendment on console: render the localized userMessage and advisory text instead of substituting raw server prose (ruling C on #32440, layer 3) #32657.
  • No breaking banner or ADR-0087 marker. Correctly, none is needed.

③ Boundary flags

Implemented-by: claude/project-thread-bhbxol
Reviewed-by: session_01FEkQ5oF8GgTHN2jBZoDwz8

VERDICT: PASS

@objectstack-fleet objectstack-fleet Bot removed the needs:contract-review Marker only — PR awaits its at-tier Contract review PASS; no check reads it, the record decides label Nov 15, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#2 · os-project-manager · session_01FEkQ5oF8GgTHN2jBZoDwz8

席位核对

Verdict: ACCEPT · head 2018f35e · 2026-11-15T01:52Z

  1. Shape. Draft → main, Fixes #32657, Refs #33524. 9 files, +435/−11:

    • ui/packages/data-objectstack/src/metadata-client.ts (MetadataValidationIssue.userMessage?) and metadata-error.ts (formatMetadataIssue)
    • ui/packages/app-shell/src/providers/saveAdvisoryToast.ts (formatFinding) and views/studio-design/metadataError.ts (issueRefusal)
    • four pin files (one per reader, plus ModalForm.markedRefusal-33524.test.tsx)
    • one root changeset (@object-ui/app-shell, @object-ui/console, @object-ui/data-objectstack patch)

    All of it is on the claimed surface, as amended (6886423383 on console: render the localized userMessage and advisory text instead of substituting raw server prose (ruling C on #32440, layer 3) #32657; console: the record create/edit dialog shows only "You don't have permission to save this record." on a controlled_by_parent master-edit refusal, dropping the API's reason ("requires edit access to its master record"); #7366 regression on this path #33524's premise correction).

  2. Premise.

  3. Declarations. Clause-②: yes (widening) (optional userMessage on the exported MetadataValidationIssue, typed from RuntimeAuthoringIssue['userMessage']), Metadata-protocol: none, patch. Contract record PASS 6887157614 on e0302529; the head since then is a base sync only.

  4. Gates on head 2018f35e (after the base sync below). All 58 check runs are success (45) or skipped (13). The dev's local --ran is 37/37, 0 NOT-MEASURED.

  5. Escalations from the record, answered. masterEditDenied's missing userMessage is server work, carried by console: the record create/edit dialog shows only "You don't have permission to save this record." on a controlled_by_parent master-edit refusal, dropping the API's reason ("requires edit access to its master record"); #7366 regression on this path #33524's re-lane. The PR's notes on Studio: an unconfigured nav item vanishes from the navigation editor after reload while still counted as a pending draft; publish dialog shows raw type names in zh #32440 (advisory half met) and console: the invite dialog names the Developer Edition member limit when cloud refuses DEVELOPER_ORGANIZATION_MEMBER_LIMIT_REACHED #32043 (member-limit sentence is cloud) are informational; no new card from this PR.

  6. K. 19 first-parent commits since base e3f0cfd4 ≥ 15: base sync done through the relay (e0302529 → 2018f35e), CI green on the synced head. Path face: not governed; 446 changed lines.

  7. Authority. Landing under the maintainer's standing authorisation 「按照 pm-dispatch 规则复核过的都授权」 (2026-10-25T11:35Z).

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

console: render the localized userMessage and advisory text instead of substituting raw server prose (ruling C on #32440, layer 3)

1 participant